package rad.framework.testing;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;
import javax.transaction.UserTransaction;

import rad.framework.embedded.ContainerTestCase;
import rad.framework.jpa.EntityPersistenceContextMap;
import rad.framework.lang.DateUtil;
import rad.framework.lang.ExceptionUtil;
import rad.framework.lang.ReflectionUtil;
import rad.framework.validator.Unique;
import rad.framework.validator.UniqueProperties;
import rad.framework.validator.ValidationUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.validator.InvalidStateException;
import org.hibernate.validator.InvalidValue;
import org.jboss.seam.persistence.PersistenceProvider;
import org.jboss.seam.transaction.Transaction;

public class JpaCrudTest<E> extends ContainerTestCase {

	protected static final Log log = LogFactory.getLog(JpaCrudTest.class);

	protected E entity;

	protected Object id;

	public JpaCrudTest(String name) {
		super(name);
	}

	public void testCrud() throws Exception {
		try {
			initData();
			persist();
			if (isUnique()) {
				duplicate();
			}
			query();
			find();
			merge();
			remove();
		} catch (Exception e) {
			Throwable cause = ExceptionUtil.getRootCause(e);
			InvalidValue[] errors = ValidationUtil.getInvalidValues(cause);
			if (errors.length > 0) {
				System.err.println("Validation Errors");
				System.err.println("-----------------");
				System.err.println(Arrays.toString(errors));
			}
			throw e;
		}
	}

	protected void initData() throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				initData(em);
			}
		}.run();
	}

	protected void initData(EntityManager em) throws Exception {
	}

	protected void persist() throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				prePersist(em);
				em.persist(entity);
				id = PersistenceProvider.instance().getId(entity,
						getEntityManager());
			}
		}.run();
	}

	protected EntityManager getEntityManager() {
		return EntityPersistenceContextMap.instance().getEntityManagerNameFor(
				getEntityClass());
	}

	protected void prePersist(EntityManager em) throws Exception {
		// setup entity
		// associations
		// TODO reflection ?
	}

	public void duplicate() throws Exception {
		try {
			// if this fails then your prePersist() is not implemented
			// sufficiently
			persist();
		} catch (InvalidStateException e) {
			System.out.println("Expected Validation Errors: "
					+ Arrays.toString(e.getInvalidValues()));
			return;
		} catch (Exception e) {
			assertFalse("Should have received a validation exception: " + e,
					true);
		}
		assertFalse("Should have received a validation exception", true);
	}

	protected void query() throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				List<?> list = em.createQuery(
						"from " + getEntityClass().getSimpleName()
								+ " where id = :id").setParameter("id", id)
						.getResultList();
				assertEquals("Query", 1, list.size());
			}
		}.run();
	}

	protected void find() throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				entity = (E) em.find(getEntityClass(), id);
				assertNotNull("Find", entity);
			}
		}.run();
	}

	protected void merge() throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				preMerge(em);
				entity = em.merge(entity);
				postMerge(em);
			}
		}.run();
	}

	protected void preMerge(EntityManager em) throws Exception {
		// change entity
	}

	protected void postMerge(EntityManager em) throws Exception {
		// change entity
	}

	protected void remove() throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				// re-attach
				entity = em.merge(entity);
				preRemove(em);
				em.remove(entity);
			}
		}.run();
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				Object o = em.find(getEntityClass(), id);
				assertNull("Remove", o);
			}
		}.run();
	}

	protected void preRemove(EntityManager em) throws Exception {
		// dis-associate
	}

	// ------------------------------------
	// helpers
	// ------------------------------------

	protected void deleteAll(final String name) throws Exception {
		new TransactionScript(getEntityClass()) {
			protected void run(EntityManager em) throws Exception {
				deleteAll(em, name);
			}
		}.run();
	}

	protected void deleteAll(EntityManager em, String name) {
		int count = em.createQuery("delete " + name).executeUpdate();
		log.info("Deleted: " + count + " : " + name);
	}

	protected Date truncateDate(Date date) {
		return DateUtil.truncateDate(date);
	}

	protected Date truncateTime(Date date) {
		return DateUtil.truncateDate(date);
	}

	protected Date addDays(int days) {
		return truncateDate(DateUtil.addDays(days));
	}

	protected Class<?> getEntityClass() {
		return ReflectionUtil.getParameterizedType(getClass());
	}

	protected boolean isUnique() {
		Class<?> type = getEntityClass();
		return type.isAnnotationPresent(UniqueProperties.class)
				|| type.isAnnotationPresent(Unique.class);
	}

	// ------------------------------------
	
	public abstract class TransactionScript extends ComponentTest {

		private Class<?> entityClass;

		public TransactionScript(Class<?> entityClass) {
			this.entityClass = entityClass;
		}

		protected void testComponents() throws Exception {
			EntityManager em = EntityPersistenceContextMap.instance()
					.getEntityManagerNameFor(entityClass);
			UserTransaction t = Transaction.instance();
			try {
				t.begin();
				em.joinTransaction();
				run(em);
				em.flush();
				t.commit();
			} catch (RuntimeException e) {
				t.rollback();
				throw e;
			} catch (Throwable e) {
				t.rollback();
				throw new Exception(e);
			}
		}

		protected abstract void run(EntityManager em) throws Exception;
	}
}
